MAPYCUS MAXIMUS

Where context meet detail

Thanh Cuong (Alex) Nguyen

Master of Business Analytics, Monash University

2025-10-10

The Problem: Seeing Everything at Once

  • Static maps show global structure but hide local detail.
  • Zooming in shows detail, but you lose context.
  • Scrolling & panning? Still fragmented.

The Problem: Seeing Everything at Once

Traditional Map

Show code
ggplot(data = vic) + 
  geom_sf(fill = "grey90", color = "grey10") +
  theme_map()

Zoom-in Map

Show code
melbourne <- vic %>% filter(LGA_NAME == "MELBOURNE")

center_bbox <- st_bbox(melbourne)

ggplot(data = vic) + 
  geom_sf(fill = "grey90", color = "grey10") +
  geom_sf_label(aes(label = LGA_NAME)) +
  coord_sf(xlim = center_bbox[c("xmin", "xmax")], ylim = center_bbox[c("ymin", "ymax")]) +
  theme_map()

HOWEVER, WHAT HAPPENED IF YOU FOCUS ZONE IS A DENSE METROPOLITAN AREA?

WILL THE PREVIOUS APPROACH STILL WORK?

Traditional Map

Show code
ggplot(data = vic_fish) +
  geom_sf(fill = "grey90", color = "grey10") +
  geom_sf(data = conn_small, aes(alpha = weight), color = "black") +
  geom_sf(data = hosp_points, color = "red", size = 1, alpha = 0.5) +
  geom_sf(data = racf_points, color = "blue", size = 1, alpha = 0.5) +
  labs(title = "Transportation between Hospital and Age Care Facilities in VIC
during COVID - 19") +
  theme_map()

Zoom-in Map

Show code
center_bbox <- st_transform(center_bbox, st_crs(vic_fish))

ggplot(data = vic_fish) +
  geom_sf(fill = "grey90", color = "grey10") +
  geom_sf(data = conn_small, aes(alpha = weight), color = "black") +
  geom_sf(data = hosp_points, color = "red", size = 1, alpha = 0.5) +
  geom_sf(data = racf_points, color = "blue", size = 1, alpha = 0.5) +
  geom_sf_label(data = hosp_points, aes(label = hosp_name), color = "red", size = 2.5, nudge_x = -1300) +
  geom_sf_label(data = racf_points, aes(label = racf_name), color = "blue", size = 2.5, nudge_x = -1500) +
  coord_sf(xlim = center_bbox[c("xmin", "xmax")], ylim = center_bbox[c("ymin", "ymax")]) +
  labs(title = "Transportation between Hospital and Age Care Facilities in VIC
during COVID - 19") +
  theme_map()

Core Functions

The transformation operates in polar coordinates:

Catersian Coordinate System

Polar Coordinate System

Core Functions

The transformation operates in polar coordinates:

\[ \begin{aligned} r &= \sqrt{(x - c_x)^2 + (y - c_y)^2} \\ \theta &= \arctan2(y - c_y, x - c_x) \end{aligned} \]

Then applies zone-specific radial mapping:

\[ r' = \begin{cases} \min(r \times z, r_{in}) & \text{if } r \leq r_{in} \text{ (focus)} \\ f_{compress}(r, s) & \text{if } r_{in} < r \leq r_{out} \text{ (glue)} \\ r & \text{if } r > r_{out} \text{ (context)} \end{cases} \]

where \(z\) = zoom_factor, \(s\) = squeeze_factor

Core Functions

Show code
grid <- create_test_grid(range = c(-1, 1), spacing = 0.1)
transform <- fisheye_fgc(grid, r_in = 0.3, r_out = 0.5, zoom_factor = 1.5, squeeze_factor = 0.5, method = "outward")
plot_fisheye_fgc(grid, transform, r_in = 0.3, r_out = 0.5)

Tip

All visualization functions use ggplot2 for easy customization

Core Functions

Show code
grid_df <- as_tibble(grid)
transform_df <- as_tibble(transform) |>
  dplyr::mutate(
    zone = attr(transform, "zones"),
    r_orig = attr(transform, "original_radius"),
    r_new  = attr(transform, "new_radius")
  )
arrows_df <- cbind(grid_df, transform_df)

ggplot() +
  # Draw arrows showing movement
  # Original points
  geom_point(
    data = grid_df, aes(x = x, y = y),
    size = 0.6, alpha = 0.7, color = "black"
  ) +
  # Transformed points
  geom_point(
    data = transform_df, aes(x = x_new, y = y_new, color = zone),
    size = 0.6, alpha = 0.7
  ) +
    geom_segment(
    data = arrows_df |> filter(zone != "context"),
    aes(x = x, y = y, xend = x_new, yend = y_new, color = zone),
    arrow = arrow(length = unit(0.02, "npc")),  # optional arrowhead
    alpha = 0.6, size = 0.5
  ) +
  scale_color_manual(values = c("focus" = "#c60000ff", 
     "glue" = "#141497ff", 
     "context" = "#FFCC00")) +
  coord_equal() +
  theme_minimal(base_size = 14) +
  labs(title = "Fisheye Transformation: Point Movement", x = "x", y = "y")

Tip

All visualization functions use ggplot2 for easy customization

Implementation - Package Architecture Overview

Package Structure

mapycusmaximus/
├── R/
│   ├── fisheye_fgc.R
│   ├── sf_fisheye.R
│   ├── sf_related.R
│   └── utils.R
├── data/
├── man/
├── vignettes/
├── tests/
└── DESCRIPTION

Design Philosophy

Three-layer architecture:

  1. Core Transformation Layer
    Mathematical fisheye operations

  2. Geospatial Integration Layer
    sf/sfc object handling

  3. Utility & Visualization Layer
    Helper functions and plotting

Modular Design

Each layer is independent and can be used separately or combined for complete workflows.

Implementation - Data Flow Through Package Layers

┌────────────────────────────────────────────────────────┐
│                    USER INPUT                          │
│              sf/sfc object + parameters                │
└─────────────────────┬──────────────────────────────────┘


┌────────────────────────────────────────────────────────┐
│         GEOSPATIAL INTEGRATION LAYER                   │
│                                                        │
sf_fisheye()                                          │
│    ├─ Auto CRS handling (EPSG:7855 or UTM)             │
│    ├─ .resolve_center() - Parse center input           │
│    ├─ Normalize coordinates to [-1,1]                  │
│    └─ Calls st_transform_custom()                      │
└─────────────────────┬──────────────────────────────────┘


┌────────────────────────────────────────────────────────┐
│           GEOMETRY HANDLER LAYER                       │
│                                                        │
st_transform_custom()                                 │
│    ├─ Iterates through geometries                      │
│    ├─ Extracts coordinates                             │
│    ├─ Calls fisheye_fgc() for transformation           │
│    ├─ Rebuilds geometries                              │
│    └─ Auto-closes polygon rings                        │
└─────────────────────┬──────────────────────────────────┘


┌────────────────────────────────────────────────────────┐
│           CORE TRANSFORMATION LAYER                    │
│                                                        │
fisheye_fgc()                                         │
│    ├─ Cartesian → Polar conversion                     │
│    ├─ Zone classification (focus/glue/context)         │
│    ├─ Radial transformation by zone                    │
│    ├─ Optional revolution (angular twist)              │
│    └─ Polar → Cartesian conversion                     │
└─────────────────────┬──────────────────────────────────┘


┌────────────────────────────────────────────────────────┐
│         GEOSPATIAL INTEGRATION LAYER                   │
│                                                        │
sf_fisheye() (continued)                              │
│    ├─ Denormalize coordinates back to map units        │
│    └─ Restore original CRS                             │
└─────────────────────┬──────────────────────────────────┘


              TRANSFORMED sf/sfc OUTPUT

┌────────────────────────────────────────────────────────┐
│         OPTIONAL: UTILITY FUNCTIONS                    │
│         (for testing & visualization)                  │
│                                                        │
│  • create_test_grid() - Generate test coordinates      │
│  • classify_zones() - Zone classification helper       │
│  • plot_fisheye_fgc() - Visualization function
└────────────────────────────────────────────────────────┘
melbourne <- vic |> 
  filter(LGA_NAME = "Melbourne")

vic_fish <- sf_fisheye(vic, 
                        center = melbourne,
                        r_in = 0.34, 
                        r_out = 0.5, 
                        zoom_factor = 20)

vic_fish |> 
  ggplot() + geom_sf()

Smart CRS Handling

Victoria region: EPSG:7855 (GDA2020 / MGA55)
Other regions: Auto-calculated UTM zones
Already projected: Uses existing CRS

MAPYCUS IN ACTION!

Show code
library(purrr)
library(dplyr)
library(stringr)

zoom_seq <- seq(1, 20, by = 0.1)
center_pt_proj <- melbourne

# Calculate melbourne centroid once
melbourne_cent <- melbourne |> st_union() |> st_centroid()

fisheye_frames <- map_dfr(zoom_seq, function(z) {
    # Apply fisheye transformations
    vic_fish_new  <- sf_fisheye(vic_fish, center = center_pt_proj,
                          r_in = 0.34, r_out = 0.5, zoom_factor = z)
    conn_fish <- sf_fisheye(conn_small, center = center_pt_proj,
                          r_in = 1.12, r_out = 5, zoom_factor = z)
    hosp_fish <- sf_fisheye(hosp_points, center = center_pt_proj,
                          r_in = 1.23, r_out = 2.3, zoom_factor = z)
    racf_fish <- sf_fisheye(racf_points, center = center_pt_proj,
                          r_in = 1.58, r_out = 2.3, zoom_factor = z)
    
    # Transform melbourne_cent to match coordinate system
    melbourne_cent_trans <- melbourne_cent |> st_transform(st_crs(hosp_fish))
    
    # Calculate distances
    distances_racf <- st_distance(racf_fish, melbourne_cent_trans) |> as.numeric() |> round()
    distances_hosp <- st_distance(hosp_fish, melbourne_cent_trans) |> as.numeric() |> round()
    
    # Add distances and counts
    racf_fish <- racf_fish |> mutate(dist = distances_racf) |> add_count(dist)
    hosp_fish <- hosp_fish |> mutate(dist = distances_hosp) |> add_count(dist)
    
    # Filter based on distance frequency
    racf_fish_filter <- racf_fish |> filter(n != max(n))
    hosp_fish_filter <- hosp_fish |> filter(n != max(n))
    
    # Filter connections
    conn_fish_filter <- conn_fish |> filter(source %in% racf_fish_filter$source)
    conn_fish_filter <- conn_fish_filter |> arrange(desc(weight)) |> head(10)
    
    # Filter based on connections
    racf_fish_filter <- racf_fish_filter |> filter(source %in% conn_fish_filter$source)
    hosp_fish_filter <- hosp_fish_filter |> filter(destination %in% conn_fish_filter$destination)
    
    # Shorten hospital names
    hosp_fish_filter <- hosp_fish_filter %>%
      mutate(hosp_name_short = hosp_name %>%
        str_remove(" Health Service$") %>%
        str_remove(" Hospital$") %>%
        str_remove(" Inc\\.$") %>%
        str_remove(" Ltd\\.$") %>%
        str_remove(" Private$") %>%
        str_remove(" Centre$") %>%
        str_remove(" Campus$") %>%
        str_remove("^The ") %>%
        str_replace(" Private Hospital", "") %>%
        str_replace(" Health$", "") %>%
        str_replace(" Rehabilitation Hospital", " Rehab") %>%
        str_replace(" Rehabilitation Centre", " Rehab") %>%
        str_replace(" Day Surgery", " Day Surg") %>%
        str_replace(" District Hospital", "") %>%
        str_replace(" Regional Hospital", " Regional") %>%
        str_squish()
      )
  
    tibble(
      zoom_factor = z,
      vic = list(vic_fish_new),
      conn = list(conn_fish_filter),
      hosp = list(hosp_fish_filter),
      racf = list(racf_fish_filter)
    ) 
})

# Reshape to long format
fish_long <- map_dfr(1:nrow(fisheye_frames), function(i) {
  z <- fisheye_frames$zoom_factor[i]
  
  bind_rows(
    fisheye_frames$vic[[i]]  %>% mutate(type = "vic",  zoom_factor = z),
    fisheye_frames$conn[[i]] %>% mutate(type = "conn", zoom_factor = z),
    fisheye_frames$hosp[[i]] %>% mutate(type = "hosp", zoom_factor = z),
    fisheye_frames$racf[[i]] %>% mutate(type = "racf", zoom_factor = z)
  )
})

library(gganimate)
library(ggplot2)

p <- ggplot() +
    # VIC map Polygon
    geom_sf(data = subset(fish_long, type == "vic"),
        fill = NA, color = "grey") +
    # Hospital labels (using short names)
    geom_sf_label(data = subset(fish_long, type == "hosp"),
        aes(label = hosp_name_short), color = "red", size = 2.5, alpha = 0.5) +
    # RACF labels
    geom_sf_label(data = subset(fish_long, type == "racf"),
        aes(label = racf_name), color = "blue", size = 2.5, alpha = 0.5) +
    # Hospitals (points)
    geom_sf(data = subset(fish_long, type == "hosp"),
        color = "red", size = 2) +
    # Age Care Facilities (points)
    geom_sf(data = subset(fish_long, type == "racf"),
        color = "blue", size = 2) +
    # Connections
    geom_sf(data = subset(fish_long, type == "conn"),
        aes(alpha = weight), color = "black") +
    coord_sf(crs = st_crs(fish_long)) +
    labs(title = "Transportation between Hospital and Age Care Facilities in Victoria during COVID-19",
         subtitle = "Zoom: {current_frame}×") +
    theme_map() +
    theme(
      plot.title = element_text(size = 15, hjust = 0.5, margin = margin(t = 10, b = 5)),
      plot.subtitle = element_text(size = 10, hjust = 0.5),
      plot.margin = margin(t = 20, r = 10, b = 10, l = 10)
    ) +
    transition_manual(zoom_factor)

anim <- animate(
  p,
  fps = 25,
  duration = 8,
  width = 1366,
  height = 768,
  res = 150
)

anim_save("fisheye_zoom_gganimate.gif", animation = anim)

MAPYCUS IN ACTION!

Rasterize Map

MAPYCUS IN ACTION!

Rasterize Map after Transform

MAPYCUS IN ACTION!

Lego Map

Percentage of the total population

Data: Observatoire des Territoires

Source: Benjamin Nowak

MAPYCUS IN ACTION!

Lego Map after Transform

Percentage of the total population

Data: Observatoire des Territoires

Source: Benjamin Nowak

MAPYCUS IN ACTION!

Melbourne Road Map

Melbourne Road Map after Transform

Use Cases & Applications

Domain Application Benefit
Urban Planning CBD-focused regional maps Detail downtown + suburban context
Transportation Route & congestion analysis Zoom bottlenecks + preserve network
Public Health Disease outbreak mapping Magnify hotspots + regional spread
Real Estate Property visualization Highlight listings + neighborhood
Emergency Incident response Detail at scene + surrounding resources
Data Viz Network graphs Focus on central nodes + topology

Future Enhancements

  • Multi-focal fisheye
    Blend multiple focus regions with weighted transitions

  • Temporal fisheye
    Animate transformations over time-series data

  • 3D extensions
    Spherical and hemispherical projections

  • AI-driven centers
    Automatic focus detection from data density

  • Interactive dashboards
    Shiny apps with real-time parameter adjustment

  • Web mapping
    Integration with leaflet/mapview

Installation & Getting Started

# Install from GitHub
devtools::install_github("Alex-Nguyen-VN/mapycusmaximus")

# Load package
library(mapycusmaximus)
library(sf)

# Quick example
data <- st_read("your_data.shp")

result <- sf_fisheye(
  data,
  center = c(lon, lat),
  center_crs = "EPSG:4326",
  r_in = 0.34,
  r_out = 0.5,
  zoom_factor = 1.5
)

# Plot
ggplot() + geom_sf(data = result)

Resources & Documentation

Key Functions

  • fisheye_fgc() : Core transformation
  • sf_fisheye() : Geospatial wrapper
  • st_transform_custom() : Geometry handler
  • plot_fisheye_fgc() : Visualization

Contributions Welcome!

Open source project seeking collaborators for enhancements and use cases

Thank You!

Transform Your Perspective – One Radius at a Time

“A cartographic lens to see both detail and context – at once”


Contact Information

📧 thanhcuong10091992@gmail.com
🔗 github.com/Alex-Nguyen-VN/mapycusmaximus

APPENDIX 1. TRANSFORMATION DETAILS

method = "expand"

Bidirectional expansion in glue zone:

  • Inner half → expands toward r_in
  • Outer half → expands toward r_out
  • Creates balanced transition
fisheye_fgc(coords,
  method = "expand",
  squeeze_factor = 0.5)

method = "outward"

Compression toward outer boundary:

  • Points hug the r_out boundary
  • Stronger compression effect
  • Good for tight packing
fisheye_fgc(coords,
  method = "outward",
  squeeze_factor = 0.3)

APPENDIX 2. Geometry Support via st_transform_custom()

Handles all standard sf geometry types:

Type Support Notes
POINT Direct coordinate transform
LINESTRING Preserves vertex order
POLYGON Auto-closes rings
MULTIPOLYGON Handles multiple parts & holes

Key Feature

Polygon rings are automatically re-closed after transformation to ensure first vertex = last vertex.

APPENDIX 3. Core Transformation Layer

fisheye_fgc.R

Primary function: fisheye_fgc()

Purpose: Pure mathematical transformation

Key operations:

  • Converts Cartesian to Polar coordinates

  • Applies zone-specific radial mapping

  • Returns transformed coordinates

Input: Numeric matrix (x, y)
Output: Transformed matrix + metadata

fisheye_fgc <- function(coords, cx = 0, cy = 0,
  r_in = 0.34, r_out = 0.5,
  zoom_factor = 1.5,
  squeeze_factor = 0.3,
  method = "expand",
  revolution = 0.0) {
  
  # Convert to polar coordinates
  dx <- coords[, 1] - cx
  dy <- coords[, 2] - cy
  radius <- sqrt(dx^2 + dy^2)
  angle <- atan2(dy, dx)
  
  # Classify into zones
  zone <- ifelse(radius <= r_in, "focus",
           ifelse(radius <= r_out, "glue", "context"))
  
  # Apply transformations...
  # Returns: matrix with x_new, y_new
}

APPENDIX 4. Geospatial Integration Layer

sf_fisheye.R

Primary function: sf_fisheye()

Purpose: Bridge between sf objects and fisheye transformation

Responsibilities:

  • Automatic CRS detection & projection

  • Center resolution (lon/lat, normalized, sf geometry)

  • Coordinate normalization/denormalization

  • Preserve aspect ratio handling

sf_related.R

Primary function: st_transform_custom()

Purpose: Generic coordinate transformer for sf geometries

Handles:

  • POINT, LINESTRING, POLYGON, MULTIPOLYGON

  • Automatic ring closure for polygons

  • Per-geometry error handling

  • Preserves CRS and attributes

Smart CRS Handling

Victoria region: EPSG:7855 (GDA2020 / MGA55)
Other regions: Auto-calculated UTM zones
Already projected: Uses existing CRS

APPENDIX 5. Utility & Visualization Layer

utils.R - Helper Functions

Testing & Setup

create_test_grid()

  • Generates regular coordinate grids

  • Useful for transformation testing

  • Customizable spacing and range

Zone Classification

classify_zones()

  • Assigns points to focus/glue/context

  • Used for visualization

  • Helpful for analysis

Visualization

plot_fisheye_fgc()

  • Side-by-side comparison

  • Color-coded by zone

  • Shows boundary circles - Built on ggplot2

# Example workflow
grid <- create_test_grid(range = c(-1, 1), spacing = 0.1)
zones <- classify_zones(grid, r_in = 0.34, r_out = 0.5)
transformed <- fisheye_fgc(grid, r_in = 0.34, r_out = 0.5)
plot_fisheye_fgc(grid, transformed, r_in = 0.34, r_out = 0.5)

APPENDIX 6. Function Relationships

Primary Functions

User-facing:

  • sf_fisheye(): Main entry point

  • fisheye_fgc():Core math

Internal:

  • st_transform_custom(): Geometry handler

  • .resolve_center():Center parser

Dependencies

sf_fisheye()
  ├─→ .resolve_center()
  ├─→ st_transform_custom()
  │    └─→ fisheye_fgc()
  └─→ st_transform() [sf pkg]

Key Design Principle

The core transformation (fisheye_fgc) is independent of sf, allowing use in non-geospatial contexts.